/**
=blacksmith/game - A game library

*/


window.GAME = {}



require(['blacksmith/three', 'blacksmith/cannon'], function() {
    
    GAME.client_firefox    = /Firefox/i.test(navigator.userAgent)
    GAME.physics_dt        = 1 / 60
    GAME.antialias         = true
    GAME.camera            = null
    GAME.target            = null
    GAME.scene             = null
    GAME.renderer          = null
    GAME.world             = null
    GAME.mouse             = {
        pos3d              : new THREE.Vector3,
        pos2d              : new THREE.Vector2,
        pointing           : null,
        dragging           : null,
        ray                : new THREE.Ray,
        projector          : new THREE.Projector(),
        intersects         : null
    }
    GAME.roots             = []
    GAME.blocks            = []
    GAME.meshes            = []
    GAME.textureMap        = {}
    GAME.noListener        = function() {}
    
})







GAME.scene = function(container, scene_size, gravity_x, gravity_y, gravity_z) {
    /**
     *>GAME.scene(container, scene_size, gravity_x, gravity_y, gravity_z)
     *
     * Initialise the scenes physical world and graphical context.
     *
     *@param container a HTMLElement
     *@param scene_size an integer that defaults to 10000
     *@param gravity_x an integer that defaults to 0
     *@param gravity_y an integer that defaults to 0
     *@param gravity_z an integer that defaults to 0
     *
     */
    
    GAME.scene_size = scene_size || 10000
    
    
    
    GAME.world = new CANNON.World()
    GAME.world.gravity.set(gravity_x || 0, gravity_y || 0, gravity_z || 0);
    GAME.world.broadphase = new CANNON.NaiveBroadphase();
    GAME.world.solver.iterations = 5;
    
    
    
    GAME.camera = new THREE.PerspectiveCamera(20, container.offsetWidth / container.offsetHeight, 1, GAME.scene_size)
    GAME.camera.position.set(0, 0, 1000)
    GAME.target = new THREE.Vector3(0, 100, 0)
    GAME.camera.lookAt(GAME.target)
    
    
    
    GAME.scene = new THREE.Scene()
    GAME.scene.add(GAME.camera)
    
    
    
    GAME.ambientLight = new THREE.AmbientLight(0x222222)
    GAME.scene.add(GAME.ambientLight)
    
    
    
    GAME.sun = new THREE.DirectionalLight(0xffffff)
    GAME.sun.position.y = GAME.scene_size*0.55
    GAME.sun.target.position.y = 0
    GAME.sun.intensity = 1.5
    GAME.sun.target.add(GAME.sun)
    GAME.scene.add(GAME.sun.target)
    
    
    
    GAME.fanY(500)
    
    
    
    window.addEventListener('resize', function(/*Event*/event) {
        GAME.camera.aspect = container.offsetWidth / container.offsetHeight
        GAME.camera.updateProjectionMatrix()
    }, false)
    
    document.addEventListener('keydown', function(/*Event*/event) {
        event.preventDefault()
        GAME.onKeyDown(event.which)
    }, false)
    
    document.addEventListener(GAME.client_firefox ? "DOMMouseScroll" : "mousewheel", function(/*Event*/event) {
        event.preventDefault()
        GAME.mousewheel_delta = GAME.client_firefox ? -event.detail / 3 : event.wheelDelta / 120
        if (GAME.mouse.dragging != null)
            GAME.mouse.dragging.onMouseWheel(GAME.mousewheel_delta)
        GAME.onMouseWheel(GAME.mousewheel_delta)
    }, false)
    
    document.addEventListener('mousedown', function(/*Event*/event) {
        event.preventDefault()
        if (GAME.mouse.dragging != null)
            GAME.mouse.dragging.onMouseDown(event.which)
		else if (GAME.mouse.pointing != null)
            GAME.mouse.pointing.onMouseDown(event.which)
        GAME.onMouseDown(event.which)
    }, false)
    
    document.addEventListener('mousemove', function(/*Event*/event) {
        event.preventDefault()
        
        GAME.mouse.pos3d.x = (((document.documentElement.scrollLeft || document.body.scrollLeft) + event.clientX) / container.offsetWidth) * 2 - 1
        GAME.mouse.pos3d.y = -((((document.documentElement.scrollTop || document.body.scrollTop) + event.clientY) / container.offsetHeight) * 2 - 1)
        GAME.mouse.pos3d.z = 0.5
        
        GAME.mouse.projector.unprojectVector(GAME.mouse.pos3d, GAME.camera)
        
        GAME.mouse.ray.direction = GAME.mouse.pos3d.subSelf(GAME.camera.position).normalize()
        
        GAME.mouse.intersects = GAME.mouse.ray.intersectObjects(GAME.meshes)
        
        if (GAME.mouse.dragging != null) {
            
            GAME.mousemove_dx = GAME.camera.position.x - GAME.mouse.dragging.mesh.position.x,
            GAME.mousemove_dy = GAME.camera.position.y - GAME.mouse.dragging.mesh.position.y,
            GAME.mousemove_dz = GAME.camera.position.z - GAME.mouse.dragging.mesh.position.z
            GAME.mouse.distance = Math.sqrt(
                (GAME.mousemove_dx * GAME.mousemove_dx) +
                (GAME.mousemove_dy * GAME.mousemove_dy) +
                (GAME.mousemove_dz * GAME.mousemove_dz)
            )
            GAME.mouse.pos2d.x = GAME.camera.position.x + GAME.mouse.distance * GAME.mouse.ray.direction.x
            GAME.mouse.pos2d.y = GAME.camera.position.z + GAME.mouse.distance * GAME.mouse.ray.direction.z
            
            if (GAME.mouse.dragging.parent != null)
                // s dragging currently attached to a parent, adjust position relative to parent
                GAME.mouse.dragging.setPosition(
                    GAME.mouse.pos2d.x - GAME.mouse.dragging.parent.mesh.position.x,
                    GAME.mouse.dragging.parent.mesh.position.y,
                    GAME.mouse.pos2d.y - GAME.mouse.dragging.parent.mesh.position.z
                )
                
            else
                // adjust position relative to world
                GAME.mouse.dragging.setPosition(
                    GAME.mouse.pos2d.x,
                    GAME.mouse.dragging.mesh.position.y,
                    GAME.mouse.pos2d.y
                );
                
            GAME.mouse.dragging.onMouseDrag()
            
        } else {
            
            if (GAME.mouse.intersects.length > 0) {
                GAME.mousemove_block = GAME.meshToBlock(GAME.mouse.intersects[0].object)
                
                if (GAME.mouse.pointing !== GAME.mousemove_block) {
                    if (GAME.mouse.pointing !== null)
                        GAME.mouse.pointing.onMouseExit()
                    GAME.mousemove_block.onMouseEnter()
                }
                
                GAME.mouse.pointing = GAME.mousemove_block
            
            } else if (GAME.mouse.pointing !== null)
                GAME.mouse.pointing.onMouseExit()
            else
                GAME.mouse.pointing = null
        }
        
        GAME.onMouseMove()
    }, false)
    
    
    
    GAME.renderer = new THREE.WebGLRenderer({antialias: GAME.antialias})
    container.appendChild(GAME.renderer.domElement)
    
    
    
    return GAME
    
}






GAME.play = function() {
    /**
     *>GAME.play()
     *
     * Start the physics and rendering updates.
     *
     */
    
    requestAnimationFrame(GAME.play)
    
    GAME.runIndex = GAME.roots.length
    
    while (GAME.runIndex--) {
        GAME.runBlock = GAME.roots[GAME.runIndex]
        GAME.runBlock.mesh.position.x = GAME.runBlock.body.position.x
        GAME.runBlock.mesh.position.y = GAME.runBlock.body.position.y
        GAME.runBlock.mesh.position.z = GAME.runBlock.body.position.z
        GAME.runBlock.mesh.quaternion.x = GAME.runBlock.body.quaternion.x
        GAME.runBlock.mesh.quaternion.y = GAME.runBlock.body.quaternion.y
        GAME.runBlock.mesh.quaternion.z = GAME.runBlock.body.quaternion.z
        GAME.runBlock.mesh.quaternion.w = GAME.runBlock.body.quaternion.w
    }
    
    GAME.renderer.render(GAME.scene, GAME.camera)
    GAME.world.step(GAME.physics_dt)
    
}





GAME.Block = function(/*Object*/properties) {
    /**
     *=GAME.Block - Block object
     *
     * A physical and visual world entity.
     *
     */
    
    var self = {}
    
    
    self.setParent = function(/*GAME.Block?=null*/parent) {
        /**
         * If parent is null, this Block becomes a root Block, which defines
         * a body in the physical world. None root Blocks define fixtures
         * attached to the roots body.
         */
        
        // if parent is specified, but the same as current parent then do nothing
        if (parent != null && parent === this.parent)
            return
        
        if (this.root != null)
            // detach this and descendant shapes from roots body
            this.detachShapes(this.root.body)
        
        if (this.body != null) {
            // currently a root block; destroy body
            this.setParent_n = GAME.world.numObjects()
            GAME.world.bodies.splice(GAME.world.bodies.indexOf(this.body), 1)
            GAME.world.collision_matrix = new Int16Array(this.setParent_n*this.setParent_n)
            this.body = null
            
            // remove from list of root blocks
            GAME.roots.splice(GAME.roots.indexOf(this), 1)
            
        } else if (this.parent != null) {
            // not currently a root block but is attached; detach from parents child list
            this.parent.blocks.splice(this.parent.blocks.indexOf(this), 1)
            
        }
        
        if (parent == null) {
            // adjust position back to world coordinates
            this.positionRelativeTo()
            
            // going to be a root block, so create root body and shapes container
            this.shapes = CANNON.Compound()
            
            // attach this mesh to scene (removing it from previous attachment if any)
            GAME.scene.add(this.mesh)
            
            // set self as root with no parent
            this.setRoot(this)
            
            // add to list of root blocks
            GAME.roots.push(this)
            
        } else {
            // adjust position relative to parent
            this.positionRelativeTo(parent)
            
            // not going to be a root Block, so set root as that of new parents
            this.root = parent
            while (this.root != null && this.root.body == null)
                this.root = this.root.parent
            this.setRoot(this.root)
            
            // attach this mesh to parents mesh
            parent.mesh.add(this.mesh)
            
            // add this block to parents children
            parent.blocks.push(this)
            
        }
        
        // set parent to new parent
        this.parent = parent
        
        // attach this and descendant shapes to root body
        if (this.root != null)
            this.attachShapes(this.root)
        
        if (parent == null) {
            
            // going to be a root block, create root body from shapes container
            this.shapes = CANNON.Compound()
            this.body = new CANNON.RigidBody(/*mass*/0, this.shapes/*, material*/)
            GAME.world.add(this.body)
            
        }
        
        return this
        
    }
    
    
    
    self.addChild = function(/*GAME.Block*/block) {
        /**
         * Adds a Block as child.
         */
        
        block.setParent(this)
        
        return this
        
    }
    
    
    
    self.positionRelativeTo = function(/*GAME.Block*/block) {
        /**
         * Set position relative to a specified GAME.Block.
         * If block is not specified or null, position is relative to world.
         */
        
        GAME.positionRelativeTo_b = this.parent
        while (GAME.positionRelativeTo_b != null) {
            this.mesh.position.x += GAME.positionRelativeTo_b.mesh.position.x
            this.mesh.position.y += this.mesh.position.y + GAME.positionRelativeTo_b.mesh.position.y
            this.mesh.position.z += this.mesh.position.z + GAME.positionRelativeTo_b.mesh.position.z
            GAME.positionRelativeTo_b = GAME.positionRelativeTo_b.parent
        }
        
        GAME.positionRelativeTo_b = block
        while (GAME.positionRelativeTo_b != null) {
            this.mesh.position.x -= this.mesh.position.x - GAME.positionRelativeTo_b.mesh.position.x
            this.mesh.position.y -= this.mesh.position.y - GAME.positionRelativeTo_b.mesh.position.y
            this.mesh.position.z -= this.mesh.position.z - GAME.positionRelativeTo_b.mesh.position.z
            GAME.positionRelativeTo_b = GAME.positionRelativeTo_b.parent
        }
        
    }
    
    
    
    self.setRoot = function(/*GAME.Block*/root) {
        /**
         * Set a Block as root.
         */
        
        this.root = root
        root.shapes.addShape(this.mesh.position, this.shape)
        
        this.setRoot_i = 0
        while (this.setRoot_i < this.blocks.length)
            this.blocks[this.setRoot_i++].setRoot(root)
        
    }
    
    
    
    self.attachShapes = function(/*CANNON.Compound*/body) {
        /**
         * Attach this Block and child Block shapes to the specified body.
         */
        
        body.addChild(this.shape, new CANNON.Vec3(
            this.mesh.position.x - body.position.x,
            this.mesh.position.y - body.position.y,
            this.mesh.position.z - body.position.z))
        
        this.attachShapes_i = 0
        while (this.attachShapes_i < this.blocks.length)
            this.blocks[this.attachShapes_i++].attachShapes(body)
        
    }
    
    
    
    self.detachShapes = function(/*CANNON.Compound*/body) {
        /**
         * Detach this Block and child Block shapes from the specified body.
         */
        
        this.detachShapes_i =  body.childShapes.indexOf(this.shape)
        body.childShapes.splice(this.detachShapes_i, 1)
        this.detachShapes_i =  body.childOffsets.indexOf(this.shape)
        body.childOffsets.splice(this.detachShapes_i, 1)
        this.detachShapes_i =  body.childOrientations.indexOf(this.shape)
        body.childOrientations.splice(this.detachShapes_i, 1)
        
        this.detachShapes_t = 0
        while (this.detachShapes_t < this.blocks.length)
            this.blocks[this.detachShapes_t++].detachShapes(body)
        
    }
    
    
    
    self.followMouse = function(/*boolean*/enable) {
        /**
         * Follow the mouse.
         */
        
        GAME.mouse.dragging = enable ? this : null
        
    }
    
    
    
    self.onMouseEnter  = GAME.noListener //function()
    
    self.onMouseExit  = GAME.noListener //function()
    
    self.onMouseDown  = GAME.noListener //function(/*integer*/button)
    
    self.onMouseWheel = GAME.noListener //function(/*integer*/delta)
    
    self.onMouseDrag  = GAME.noListener //function()
    
    
    
    self.setPosition = function(/*float*/x, /*float*/y, /*float*/z) {
        /**
         ** Move to specified location.
         */
        
        this.mesh.position.set(x, y, z)
        
        if (this.body != null) {
            this.body.position.x = x
            this.body.position.y = y
            this.body.position.z = z
        }
        
    }
    
    
    
    self.name = (properties && properties.name) || 'BLOCK'
    self.fixed = (properties && properties.fixed) || false
    self.width = (properties && properties.width) || 1
    self.height = (properties && properties.height) || 1
    self.depth = (properties && properties.depth) || 1
    self.faces = (properties && properties.faces) || []
    self.map = (properties && properties.map) || null
    self.colour = (properties && properties.colour) || 0xffffff
    self.opacity = (properties && properties.opacity) || 1
    self.ambient = (properties && properties.ambient) || 0x666666
    self.shininess = (properties && properties.shininess) || 100
    self.specular = (properties && properties.specular) || 0x222222
    self.active = (properties && properties.active) || false
    self.density = (properties && properties.density) || 1
    self.friction = (properties && properties.friction) || 0.2
    self.restitution = (properties && properties.restitution) || 0.3
    self.damping = (properties && properties.damping) || 1
    
    var i = 0,
        faces = []
    
    for (; i < self.faces.length; i++) {
        self.faces[i].map = self.faces[i].map || null,
        self.faces[i].colour = self.faces[i].colour || 0xffffff,
        self.faces[i].opacity = self.faces[i].opacity || 1,
        self.faces[i].ambient = self.faces[i].ambient || 0x666666,
        self.faces[i].shininess = self.faces[i].shininess || 100,
        self.faces[i].specular = self.faces[i].specular || 0x222222,
        faces.push(
            new THREE.MeshPhongMaterial({
                color: self.faces[i].colour,
                opacity: self.faces[i].opacity,
                shading: THREE.FlatShading,
                ambient: self.faces[i].ambient,
                shininess: self.faces[i].shininess,
                specular: self.faces[i].specular,
                map: self.faces[i].map
            })
        )
    }
    
    
    self.mesh = new THREE.Mesh(
        (faces.length > 0) ? new THREE.CubeGeometry(self.width, self.height, self.depth, faces) :
        new THREE.CubeGeometry(self.width, self.height, self.depth),
        new THREE.MeshPhongMaterial({
            color: self.colour,
            opacity: self.opacity,
            shading: THREE.FlatShading,
            ambient: self.ambient,
            shininess: self.shininess,
            specular: self.specular,
            map: self.map
        })
    )
    self.mesh.useQuaternion = true
    self.mesh.castShadow = true
    self.mesh.receiveShadow  = true
    
    
    self.shape = new CANNON.Box(new CANNON.Vec3(this.width*0.5, this.height*0.5, this.depth*0.5))
    
    
    self.setPosition(
        (properties && properties.x) || 0,
        (properties && properties.y) || 0,
        (properties && properties.z) || 0
    )
    
    GAME.scene.add(self.mesh)
    GAME.meshes.push(self.mesh)
    GAME.blocks.push(self)
    
    self.blocks = []
    
    return self
    
}


GAME.meshToBlock = function(/*THREE.Mesh*/mesh) {
    /**
     **Returns the Block of the specified THREE.Mesh.
     */
    
    GAME.meshToBlockIndex = GAME.blocks.length
    
    while (GAME.meshToBlockIndex--) {
        if (GAME.blocks[GAME.meshToBlockIndex].mesh === mesh)
            return GAME.blocks[GAME.meshToBlockIndex]
    }
    
    return null
}


GAME.onMouseMove = GAME.noListener //function()


GAME.onMouseDown = GAME.noListener //function(/*integer*/button)


GAME.onMouseWheel = GAME.noListener //function(/*integer*/delta)


GAME.onKeyDown = GAME.noListener //function(/*integer*/key)


GAME.shadows = function(enable) {
    if (enable) {
        GAME.renderer.shadowMapEnabled = true
        GAME.renderer.shadowMapSoft = true
        GAME.renderer.shadowMapCullFrontFaces = false
        GAME.sun.castShadow = true
        GAME.sun.shadowDarkness = 0.5
        GAME.sun.shadowMapWidth = 2048
        GAME.sun.shadowMapHeight = 2048
        GAME.sun.shadowBias = -0.003
        GAME.sun.shadowCameraFar = GAME.scene_size
        GAME.sun.shadowCameraLeft = -GAME.scene_size*0.5
        GAME.sun.shadowCameraRight = GAME.scene_size*0.5
        GAME.sun.shadowCameraTop = GAME.scene_size*0.5
        GAME.sun.shadowCameraBottom = -GAME.scene_size*0.5
    } else {
        GAME.renderer.shadowMapEnabled = false
    }
}


GAME.updateCamera = function() {
    /**
     * Update camera dependants
     */
    
    GAME.camera.lookAt(GAME.target)
    
    GAME.mouse.ray.origin.x = GAME.camera.position.x
    GAME.mouse.ray.origin.y = GAME.camera.position.y
    GAME.mouse.ray.origin.z = GAME.camera.position.z
    
}


GAME.shiftX = function(delta) {
    /**
     * Shift the camera and target along the X axis.
     */
    
    GAME.camera.position.x += delta
    GAME.target.x += delta
    GAME.sun.target.position.x = GAME.target.x
    GAME.updateCamera()
    
}


GAME.shiftY = function(delta) {
    /**
     * Shift the camera and target along the Y axis.
     */
    
    GAME.camera.position.y += delta
    GAME.target.y += delta
    GAME.sun.target.position.y = GAME.target.y
    GAME.updateCamera()
    
}


GAME.fanY = function(delta) {
    /**
     * Fan to camera along the Y axis.
     *
     * Fanning moves the position of the camera on the axis without moving its target.
     */
    
    GAME.camera.position.y += delta
    GAME.updateCamera()
    
}



GAME.shiftZ = function(delta) {
    /**
     * Shift the camera and target along the Z axis.
     */
    
    GAME.camera.position.z += delta
    GAME.target.z += delta
    GAME.sun.target.position.z = GAME.target.z
    GAME.updateCamera()
}



GAME.textures = function(/*Array*/urls, /*Function*/callback) {
    
    var images = {},
        count = 0
    
    urls.forEach(function(url, i) {
        if (GAME.textureMap.hasOwnProperty(url)) {
            count++
            if (count === urls.length)
                callback(GAME.textureMap)
            return
        }
        
        images[url] = new Image
        
        images[url].onload = function() {
            var texture = new THREE.Texture(images[url])
            texture.needsUpdate = true
            images[url] = null
            GAME.textureMap[url] = texture
            count++
            if (count === urls.length)
                callback(GAME.textureMap)
        }
        
        images[url].src = url
    })
    
}

